S03-01 核心类-Object
[TOC]
概述
什么是 Object
Object:Java 所有类的“根父类”:所有类(自定义类、内置类、数组)都直接或间接继承它。
- 自动继承:任何没有显式声明父类的类,默认继承
java.lang.Object(比如class Person {}等价于class Person extends Object {}); - 间接继承:显式继承的类会通过继承链间接继承 Object(如
class Dog extends Animal→Animal继承 Object →Dog间接继承 Object); - 特殊类型也继承:数组、枚举、注解等特殊类型也属于 Object 类型(如
String[] arr = new String[5]; arr instanceof Object返回true)。
本质:统一所有对象的基础行为:
Object 类封装了所有 Java 对象的通用能力,比如:
- 判断对象是否相等(
equals()); - 生成对象的哈希值(
hashCode()); - 描述对象信息(
toString()); - 线程间通信(
wait()/notify())。
这些方法保证了所有 Java 对象都具备统一的基础接口,是面向对象设计的“最小约定”。
核心特性
核心特性:
- 无需手动导入:
java.lang包下的类(包括 Object)由 JVM 自动导入,可直接使用; - 仅含无参构造:Object 只有一个无参构造方法,子类构造方法默认通过
super()调用它; - 不可被重写的核心方法:
getClass()、wait()、notify()等被final修饰,无法子类重写。
核心方法
Object 类的核心方法(逐个详解):
Object 类的方法可分为 5 大类,以下是每个方法的签名、默认实现、作用、重写规则和实战示例(新手重点掌握前 4 个方法):
== 运算符
在 Java 中,== 是一个比较运算符(Comparison Operator)。
虽然它看起来很简单,但它是 Java 新手最容易混淆的概念之一,因为它在处理基本数据类型和引用数据类型时,行为截然不同。
我们可以用一句话总结它的核心逻辑:
==永远比较的是变量内存中存储的“值”。
只是对于不同类型,“内存中的值”代表的意义不同。
基本数据类型
场景 1:基本数据类型 (Primitives):
(byte, short, int, long, float, double, char, boolean)
对于基本数据类型,变量在栈内存(Stack)中直接存储的是实际的数据值。
- 行为:
==比较的是它们的数值是否相等。 - 规则: 只要数值一样,结果就是
true。
代码示例:
int a = 10;
int b = 10;
double c = 10.0;
System.out.println(a == b); // true (10 等于 10)
System.out.println(a == c); // true (10 等于 10.0,自动类型提升后数值相等)引用数据类型
场景 2:引用数据类型 (Reference Types):
(类、接口、数组)
对于引用数据类型,变量在栈内存(Stack)中存储的不是对象本身,而是对象在堆内存(Heap)中的内存地址(Reference)。
- 行为:
==比较的是它们的内存地址。 - 本质: 它判断的是“这两个引用是否指向同一个物理对象”。
- 比喻: 就像你有两把钥匙。
==问的是:“这两把钥匙是不是同一把(完全一模一样的那把金属)?”,而不是问“它们能不能开同一扇门?”。
代码示例:
// 在堆内存中创建了两个不同的对象,虽然内容一样
User u1 = new User("张三");
User u2 = new User("张三");
User u3 = u1; // u3 复制了 u1 的地址
System.out.println(u1 == u2); // false (地址不同,是两个独立的对象)
System.out.println(u1 == u3); // true (指向同一个对象)陷阱:String 字符串
特殊陷阱:String 字符串:
这是面试中关于 == 最著名的坑。由于 Java 对字符串进行了特殊优化(字符串常量池 String Constant Pool),导致 == 的结果有时让人困惑。
方式一:字面量创建 (Literal):
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true!原因: Java 发现常量池里已经有 "Hello" 了,就直接把 s2 指向了同一个地址。为了省内存,它们共享了同一个对象。
方式二:new 关键字创建:
String s3 = new String("Hello");
String s4 = new String("Hello");
System.out.println(s3 == s4); // false!原因: new 关键字强制在堆内存中开辟一块新空间。不管内容是不是一样,地址绝对不同。
陷阱:包装类的缓存
进阶陷阱:包装类的缓存 (Integer Cache):
这是另一个高频考点。Java 的包装类(Integer, Long 等)也有缓存机制。
Integer i1 = 100;
Integer i2 = 100;
System.out.println(i1 == i2); // true (命中缓存 -128 到 127)
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i3 == i4); // false (超出缓存范围,创建了新对象)结论: 比较包装类对象时,永远不要用 ==,一定要用 equals(),否则可能会遇到这种“薛定谔的相等”。
== vs equals()
:== vs equals() 终极对比
| 维度 | == 运算符 | equals() 方法 |
|---|---|---|
| 类型 | Java 语言内置的操作符 | Object 类定义的方法 |
| 基本数据类型 | 比较数值 (可用) | 编译错误 (不可用) |
| 引用数据类型 | 比较内存地址 (判断是否同一对象) | 默认比地址,重写后比内容 |
| 运行速度 | 极快 (CPU 指令级比较) | 较慢 (涉及方法调用和逻辑判断) |
| 空指针安全 | 安全 (null == null 为 true) | 不安全 (调用 null.equals 会崩) |
最佳实践总结
最佳实践总结:
- 基本类型: 只能用
==。 - 字符串 (String): 死都别用
==,一定要用equals()。 - 包装类 (Integer, Long): 别用
==,用equals(),以防缓存坑。 - 枚举 (Enum): 可以用
==,也可以用equals()(枚举单例不仅值相等,地址也相等,官方甚至建议用==以避免空指针)。 - 自定义对象:
- 想判断是不是“同一个对象实例”?用
==。 - 想判断“业务含义是否相等”?用
equals()(前提是重写了它)。
toString()
public String toString():返回该对象的字符串表示。
- 无参数
- 返回:
String,该对象的文本描述。结果应是一个“简洁但内容丰富,且易于阅读”的字符串。 - 抛出:无。
基本示例:
默认实现 vs 重写实现:
class User {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
// 场景2:重写 toString (推荐)
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
}
}
public class Main {
public static void main(String[] args) {
Object rawObj = new Object();
User user = new User(1, "Alice");
// 场景1:默认实现 (类名 + @ + 十六进制哈希)
// 输出示例: java.lang.Object@74a14482
System.out.println(rawObj.toString());
// 场景2:重写后输出业务数据
// 输出: User{id=1, name='Alice'}
System.out.println(user); // 自动调用 toString()
}
}核心特性:
默认实现的剖析:
在
Object类中,toString()的源代码如下:javapublic String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }它由三部分组成:
- 类名:全限定类名(如
java.lang.Object)。 - 分隔符:
@符号。 - 身份标识:对象的哈希码的无符号十六进制表示。
深度解读:对于未重写该方法的对象,打印出来的字符串(如
User@4554617c)实际上是该对象在 JVM 堆内存中的逻辑身份指纹。虽然它通常不直接等同于物理内存地址, 但在 HotSpot 虚拟机默认的 hashCode 实现中,它与对象的内存地址紧密相关。- 类名:全限定类名(如
隐式调用机制:
toString()是 Java 中被调用最频繁的方法之一,因为它被深度集成到了语言特性中。- 字符串拼接:
"Data: " + obj编译器会自动将被拼接对象转换为obj.toString()(若为 null 则拼接 "null")。 - 打印输出:
System.out.println(obj)内部直接调用String.valueOf(obj),进而调用toString()。 - 调试工具:IDE(IntelliJ IDEA, Eclipse)在 Debug 模式下的变量视图中,默认展示的就是对象的
toString()结果。
- 字符串拼接:
注意事项:
无限递归(StackOverflowError):
这是重写
toString时最致命的坑。如果两个对象存在双向引用(例如:父子节点、ORM 框架中的多对一关系),且两者的toString都试图打印对方,就会瞬间导致栈溢出。javaclass Parent { Child child; @Override public String toString() { return "Parent{child=" + child + "}"; } } class Child { Parent parent; @Override public String toString() { return "Child{parent=" + parent + "}"; } // 💣 触发无限递归 }解决方案:打破引用链。在
toString中,对于反向引用的字段,不要直接打印对象,而是打印其 ID 或名称,或者使用@ToString.Exclude(Lombok) 排除该字段。数组的打印陷阱:
Java 数组也是对象,但它们没有重写
toString()。直接打印数组会输出难以理解的类型签名(如[I@1b6d3586)。javaint[] nums = {1, 2, 3}; System.out.println(nums); // 输出 [I@xxxx (无意义) // 正确做法: System.out.println(java.util.Arrays.toString(nums)); // 输出 [1, 2, 3]敏感信息泄露:
日志系统通常会记录对象的
toString结果。切勿在toString中包含密码、密钥、身份证号等敏感字段。
扩展知识:
自动化生成工具:
手写
toString既繁琐又容易在新增字段时遗漏。推荐使用以下方案:Lombok:使用
@ToString注解,编译期自动生成代码。javaimport lombok.ToString; @ToString public class User { private String name; private int age; // 编译时,Lombok 会自动帮你生成标准的 toString 代码 }IDE 生成:IDEA 中
Alt + Insert->toString()。java@Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + ", email='" + email + '\'' + '}'; }Apache Commons Lang:
ToStringBuilder.reflectionToString(this)(通过反射实现,方便但性能较差,生产环境慎用高频调用)。
Java 14+ Records (记录类):
Java 14 引入的
record关键字定义的数据类,编译器会自动生成包含所有字段值的标准toString(),格式清晰且规范。javapublic record Point(int x, int y) {} // 自动生成的 toString 输出:Point[x=10, y=20]
equals()
public boolean equals(Object obj):指示其他某个对象是否与此对象“逻辑相等”。
- obj:
Object,要与之进行比较的引用对象。 - 返回:如果此对象与 obj 参数在逻辑上相同,则返回
true;否则返回false。 - 抛出:无(但在实现不当时可能抛出
NullPointerException或ClassCastException)。
基本示例:
最佳实践模板:以下是遵循《Effective Java》建议的标准覆写模板,兼顾了性能、健壮性和类型安全。
public class Employee {
private long id;
private String name;
private double salary;
@Override
public boolean equals(Object o) {
// 1. 自反性检查:如果是同一个对象引用,直接返回 true(性能优化)
if (this == o) return true;
// 2. 类型检查:处理 null 并确保类型兼容
// 注意:这里使用 instanceof 允许子类相等(如 Hibernate 代理对象),
// 若要求严格类型匹配(拒绝子类),则改用 if (o == null || getClass() != o.getClass())
if (!(o instanceof Employee)) return false;
// 3. 类型转换
Employee employee = (Employee) o;
// 4. 关键域比较:基本类型用 ==,引用类型用 Objects.equals,浮点型用 Float/Double.compare
return id == employee.id &&
Double.compare(employee.salary, salary) == 0 && // 处理 NaN 和 -0.0
java.util.Objects.equals(name, employee.name);
}
// 警告:覆写 equals 必须覆写 hashCode
@Override
public int hashCode() {
return java.util.Objects.hash(id, name, salary);
}
}核心特性:
引用相等 vs 逻辑相等:
Object类中默认的equals实现仅仅是检查引用相等性(Reference Equality),即比较内存地址(this == obj)。然而,对于像
String、Integer、Date以及自定义的数据类(DTO/Entity),我们通常关注的是逻辑相等性(Logical Equality)。即只要两个对象的关键状态(成员变量)一致,它们就被视为相等,即使它们存储在堆内存的不同位置。数学上的等价关系(Equivalence Relation):
Java 语言规范(JLS)严格定义了
equals方法必须满足的五个特性。任何违反这些特性的实现都会导致 HashMap、HashSet 或 Collections.sort 等基础类库出现不可预知的行为。自反性 (Reflexive):对于任何非空引用 x,
x.equals(x)必须返回 true。对称性 (Symmetric):对于任何非空引用 x 和 y,当且仅当
y.equals(x)返回 true 时,x.equals(y)必须返回 true。传递性 (Transitive):如果
x.equals(y)为 true 且y.equals(z)为 true,则x.equals(z)必须返回 true。一致性 (Consistent):对于任何非空引用 x 和 y,只要对象中的信息没有被修改,多次调用
x.equals(y)必须一致地返回 true 或 false。非空性 (Non-nullity):对于任何非空引用 x,
x.equals(null)必须返回 false。
注意事项:
hashCode 协定 (The Golden Rule):
这是 Java 中最常见也是最严重的 Bug 来源之一。
契约规定:如果两个对象根据
equals(Object)方法是相等的,那么调用这两个对象中任一对象的hashCode方法必须产生相同的整数结果。如果在覆写
equals时忽略了hashCode,使用该类的对象作为HashMap的 Key 或存入HashSet时,会导致哈希逃逸(Hash Escape):即使两个对象逻辑相同,由于哈希值不同,它们会被映射到哈希表的不同桶(Bucket)中,导致get()返回 null 或add()产生重复数据。java// 错误示范:只重写 equals 未重写 hashCode Person p1 = new Person(1, "Jack"); Person p2 = new Person(1, "Jack"); Map<Person, String> map = new HashMap<>(); map.put(p1, "Data"); // 灾难现场:虽然 p1.equals(p2) 为 true,但 p2 计算出的 hash 不同 // 导致无法在 Map 中定位到 p1 存放的数据 System.out.println(map.get(p2)); // 输出 null浮点数的特殊性:
对于
float和double类型的字段,切勿直接使用==进行比较。Float.NaN和Double.NaN不等于自身(NaN == NaN为 false),但equals规范要求自反性。0.0f和-0.0f是相等的(==为 true),但它们在哈希表中应该被视为不同的键。
解决方案:使用
Float.compare(f1, f2)或Double.compare(d1, d2),或者直接转换成long(Double.doubleToLongBits) 进行比较。继承中的对称性崩塌:
当一个类继承自通过
instanceof实现equals的父类,并添加了新的值组件(Value Component)时,无法同时满足对称性和传递性。- 如果子类
equals试图比较新字段,则super.equals(sub)忽略新字段返回 true,而sub.equals(super)检查新字段返回 false(违反对称性)。 - 如果为了满足对称性忽略类型差异,往往会牺牲传递性。
解决方案:
使用 组合优先于继承(Composition over Inheritance)。
如果必须继承,且父类和子类都需要参与比较,考虑将
equals定义为final,或者在父类中使用getClass()代替instanceof(但这违反了里氏替换原则,导致子类对象无法等于父类对象)。
- 如果子类
扩展知识:
Lombok 与 AutoValue:
在实际工程中,手写
equals和hashCode容易出错且繁琐。强烈建议使用 Lombok 的@EqualsAndHashCode注解或 Google 的 AutoValue 框架自动生成代码。Java 14+ Records:
Java 14 引入的
record(记录类型)会自动生成正确的equals、hashCode和toString方法,非常适合用于仅作为数据载体的类(DTO)。javapublic record Point(int x, int y) { } // 自动拥有基于 x 和 y 的 equals 实现
hashCode()
public native int hashCode():返回该对象的哈希码数值。
- 参数:无。
- 返回:
int,一个代表该对象特征的 32 位整数。 - 抛出:无。
基本示例:
标准实现范式:以下展示了不依赖第三方库的手动实现方式(性能最优),以及现代 IDE 自动生成的标准逻辑。
public class User {
private int id;
private String name;
private boolean active;
@Override
public int hashCode() {
// 1. 初始化一个非零常数 (通常是质数,如 17)
int result = 17;
// 2. 为每个参与 equals 的字段计算哈希并累加
// 核心公式:result = 31 * result + fieldHash
// 基本类型直接转换或计算
result = 31 * result + id;
result = 31 * result + (active ? 1 : 0);
// 引用类型调用其自身的 hashCode (需处理 null)
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
// 简易写法 (性能稍差,因为会创建 Object[] 数组导致 GC 压力):
// return Objects.hash(id, name, active);
}核心特性:
协定与一致性 (The Contract):
hashCode的核心存在意义是为了支撑基于散列的集合(Hash-based Collections,如HashMap,HashSet,ConcurrentHashMap)的高效运作。Java 规范对其有严格的三条协定:- 一致性:在 Java 应用的一次执行过程中,只要对象
equals比较所用的信息未被修改,多次调用hashCode必须返回相同的整数。 - 相等性映射:如果
x.equals(y)返回true,则x和y的hashCode()必须相等。 - 碰撞允许性:如果
x.equals(y)返回false,并不要求x和y的hashCode()必须不同。但在哈希表中,不相等的对象具有不同的哈希码可以显著提高性能(减少哈希碰撞)。
javapublic static void main(String[] args) { AA aa = new AA(); AA aa2 = new AA(); AA aa3 = aa; // 1. aa 和 aa2 指向不同的对象,它们的 hashCode 值也不同 System.out.println(aa.hashCode() == aa2.hashCode()); // false // 2. aa 和 aa3 指向同一个对象,它们的 hashCode 值相同 System.out.println(aa.hashCode() == aa3.hashCode()); // true }- 一致性:在 Java 应用的一次执行过程中,只要对象
为什么是 31 (The Magic Number):
在手动生成
hashCode时,你会发现几乎所有的 IDE(IntelliJ, Eclipse)和 JDK 源码(如String类)都使用 31 作为乘数。- 质数特性:31 是一个奇质数。如果乘数是偶数,乘法溢出相当于左移,会导致信息丢失(低位被补 0)。质数能更好地保留各字段的特征,降低哈希碰撞概率。
- 位运算优化:31 有一个很好的位运算特性,即
31 * i == (i << 5) - i。现代 JVM 的 JIT 编译器能自动将乘法优化为移位和减法操作,在底层 CPU 指令级别执行效率极高。
默认行为 (Identity Hash Code):
如果一个类没有重写
hashCode(),它将调用Object类的本地 (native) 实现。- 在 HotSpot JVM 中,默认实现并不总是直接返回内存地址(因为对象在 GC 时会移动,地址会变)。
- 它通常是基于线程状态、随机数或内存地址计算出的一个身份哈希码,并存储在对象头(Object Header)的 Mark Word 中。一旦计算过一次,该值就会被固化在对象头中,即使对象被 GC 移动,该值也不再改变。
注意事项:
可变对象的哈希灾难:
切勿使用可变字段作为计算 hashCode 的依据,尤其是在该对象已经存入 Set 或 Map 之后。
这是新手最容易踩的坑。如果在对象存入
HashMap后修改了参与hashCode计算的字段,对象的哈希值会发生变化,但它在 Map 中的位置(桶下标)还是旧的。这会导致你明明拿着完全相同的对象去get或remove,却返回null或无法删除,造成内存泄漏。java// 错误示范:修改了参与 hash 计算的字段 User u = new User(1, "Jack"); Set<User> set = new HashSet<>(); set.add(u); // 此时根据 hash(Jack) 放入 bucket A u.setName("Rose"); // 修改字段,hashCode 变了! // 虽然 u 还在 set 里,但 contains 会去 bucket B 找,结果找不到 System.out.println(set.contains(u)); // 输出 false (这是个严重的 Bug)DoS 攻击风险:
如果你的
hashCode实现过于简单(例如直接返回字段值)或可预测,恶意用户可以构造大量具有相同哈希值的对象(哈希碰撞)。这将导致HashMap退化为链表(或红黑树),将查找时间复杂度从 O(1) 拖慢至 O(n) 或 O(log n),从而消耗大量 CPU 资源,造成 Hash DoS 攻击。- 防护:JDK 8 之后的
HashMap引入了红黑树机制来缓解此问题,但在高安全需求的场景下,需谨慎设计哈希算法。
- 防护:JDK 8 之后的
性能权衡:
- 缓存哈希值:对于不可变类(Immutable Class,如
String),如果在hashCode计算开销很大,建议将计算结果缓存起来(Lazy Initialization)。 - 避免自动装箱:在高性能场景下,尽量避免使用
Objects.hash()处理基本数据类型,因为它会触发自动装箱和数组创建。
- 缓存哈希值:对于不可变类(Immutable Class,如
扩展知识:
System.identityHashCode():
即使一个对象重写了
hashCode(),你依然可以通过System.identityHashCode(obj)获取该对象原本的、由 JVM 提供的默认哈希码。这在判断两个引用是否指向绝对同一个对象实例(不仅仅是逻辑相等)时非常有用。javaString s = new String("Hello"); // String 重写了 hashCode,基于内容计算 System.out.println(s.hashCode()); // 获取基于对象身份的原始 hash,类似 Object.hashCode() System.out.println(System.identityHashCode(s));
getClass()
public final native Class<?> getClass():返回此 Object 的运行时类。
- 无参数
- 返回:
Class<?>,表示此对象运行时类的Class对象。返回的Class对象是被该对象所属的类加载器加载的那个单例对象。 - 抛出:无(但在
null对象上调用会抛出NullPointerException)。
基本示例:
示例关键词:多态、运行时类型识别
public class Main {
public static void main(String[] args) {
Object str = "Hello Java";
Object num = 100; // 自动装箱为 Integer
// 尽管引用类型都是 Object,但 getClass() 返回实际的运行时类型
System.out.println("str 运行时类: " + str.getClass().getName());
// 输出: java.lang.String
System.out.println("num 运行时类: " + num.getClass().getName());
// 输出: java.lang.Integer
// 验证 Class 对象的单例性
System.out.println(str.getClass() == String.class); // true
}
}核心特性:
运行时类型识别 (RTTI) 与多态:
getClass()是 Java 运行时类型识别(RTTI)的核心机制之一。与引用变量声明的静态类型(Static Type)不同,getClass()返回的是对象在堆内存中实际创建的动态类型(Dynamic Type)。这意味着,即使你将一个子类对象赋值给父类引用,
getClass()依然能洞察其本质。这是通过 JVM 在对象头中维护元数据来实现的。JVM 底层实现:对象头与 Klass Pointer:
该方法是一个
native方法,其实现直接映射到 JVM 内部。- 对象布局:在 HotSpot 虚拟机中,Java 对象在堆内存中的布局包含“对象头”(Object Header)。
- 类型指针:对象头中包含一个类型指针(Klass Pointer /
_klass),它指向方法区(Metaspace)中该类的元数据对象(Klass结构)。 - 映射机制:当你调用
getClass()时,JVM 不会去分析代码,而是直接读取对象头中的这个指针,找到对应的Class对象并返回。这就是为什么它非常快且不可伪造。
Final 不可重写性:
该方法被声明为
final。这是为了保证 Java 类型系统的安全性和一致性。如果允许子类重写getClass()并返回错误的类型(例如String的子类谎称自己是Integer),将导致 JVM 的类型检查机制、反射机制以及安全沙箱完全崩溃。
注意事项:
泛型类型擦除 (Type Erasure):
这是
getClass()最常见的误区。Java 的泛型是伪泛型,在编译后会进行类型擦除。因此,你无法通过getClass()获取泛型的具体参数类型。javaList<String> list1 = new ArrayList<>(); List<Integer> list2 = new ArrayList<>(); // 看起来是不同的类型,但运行时类完全相同 System.out.println(list1.getClass() == list2.getClass()); // 输出 true System.out.println(list1.getClass()); // 输出 class java.util.ArrayList // 无法获取 <String> 或 <Integer> 信息equals() 方法中的使用陷阱:getClass vs instanceof:
在重写
equals方法时,使用getClass()还是instanceof决定了比较的严格程度。getClass():要求两个对象必须是完全相同的类。这违反了里氏替换原则(LSP),因为子类无法等于父类,即便它们逻辑上相等。instanceof:允许子类与父类进行比较(只要子类是父类的实例)。javaclass Point { int x, y; } class ColorPoint extends Point { int color; } Point p = new Point(); ColorPoint cp = new ColorPoint(); // 如果 Point.equals 使用 getClass(): p.equals(cp); // return false (因为 Point.class != ColorPoint.class) // 如果 Point.equals 使用 instanceof: p.equals(cp); // 可能 return true (取决于逻辑,但类型检查是通过的)
建议:如果你希望子类对象在逻辑上等同于父类对象(如 Hibernate 代理对象),请慎用
getClass(),改用instanceof。如果你需要绝对的类型匹配,才使用getClass()。空指针异常风险:
getClass()是实例方法,必须通过对象实例调用。如果引用为null,JVM 无法通过对象头寻找类型信息,因此会立即抛出NullPointerException。javaObject obj = null; // obj.getClass(); // 抛出 NPE // 安全替代方案(仅用于检查,非获取 Class) // java.util.Objects.requireNonNull(obj);
扩展知识:
关联知识:
.class字面量 vsgetClass():ClassName.class:在编译期确定。如果类尚未加载,会触发类加载器加载该类,但不会初始化(不执行静态代码块,取决于具体 JVM 实现和使用场景,通常仅引用.class不会触发初始化,但Class.forName会)。obj.getClass():在运行期确定。前提是必须先有一个实例化对象。
数组的特殊性:
数组也是对象,也有
getClass()。不同维度的数组、不同类型的数组,其Class对象是不同的。javaint[] arr1 = new int[10]; int[][] arr2 = new int[10][10]; double[] arr3 = new double[10]; System.out.println(arr1.getClass().getName()); // [I (代表 int[]) System.out.println(arr2.getClass().getName()); // [[I (代表 int[][]) System.out.println(arr1.getClass() == arr2.getClass()); // false反射入口:
getClass()通常是反射操作的第一步。拿到Class对象后,你可以调用getMethods(),getDeclaredFields(),getConstructor()等方法来动态操纵代码。
clone()
protected native Object clone():创建并返回此对象的一个副本。
- 无参数
- 返回:
Object,该对象的拷贝。 - 抛出:
CloneNotSupportedException- 如果对象的类不支持Cloneable接口,则抛出此异常。
基本示例:
示例关键词:实现 Cloneable、提升访问权限
// 1. 必须实现 Cloneable 接口(标记接口),否则抛异常
class Person implements Cloneable {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 2. 必须重写 clone 方法,并将 protected 改为 public
@Override
public Object clone() {
try {
// 3. 调用 super.clone() 获取底层拷贝
return super.clone();
} catch (CloneNotSupportedException e) {
// 理论上不会发生,因为我们实现了 Cloneable
throw new AssertionError();
}
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("Java", 25);
// 调用 public 的 clone 方法
Person p2 = (Person) p1.clone();
System.out.println(p1 != p2); // true (地址不同)
System.out.println(p1.getClass() == p2.getClass()); // true
}
}核心特性:
浅拷贝 (Shallow Copy) 机制:
Object.clone()的默认实现是浅拷贝。底层逻辑:JVM 会在堆内存中开辟一块新的内存空间,大小与原对象一致,然后直接将原对象内存块中的二进制数据(Bitwise Copy)复制到新内存中。
值传递:
对于基本数据类型(int, long, boolean 等),复制其具体的值。
对于引用数据类型(String, List, 对象引用),复制的是内存地址(引用)。
后果:新旧对象共享同一个引用类型的成员变量。如果其中一个修改了该引用指向的可变对象内部数据,另一个也会受到影响。
Cloneable 接口的魔法:
Cloneable是一个没有任何方法的标记接口(Marker Interface)。JVM 检查:当调用
Object.clone()时,JVM 会检查该对象的类是否实现了Cloneable。行为差异:
若已实现:JVM 执行内存复制操作。
若未实现:JVM 直接抛出
CloneNotSupportedException。设计缺陷:通常接口定义的是“能做什么”(Capabilities),但
Cloneable却修改了父类Object中clone方法的行为,这被公认为 Java 早期的一个设计瑕疵。
不调用构造器:
clone()是极其特殊的实例化方式。它不会调用构造方法。- 原理:它是直接的内存复制。
- 风险:如果你的构造器中有复杂的初始化逻辑(如计数器自增、资源绑定、依赖注入),使用
clone()将会绕过这些逻辑,产生不完整的对象状态。
注意事项:
深拷贝 (Deep Copy) 的实现难题:
如果对象包含可变的引用类型字段(如
ArrayList、Date或自定义对象),必须手动实现深拷贝,否则会产生严重的副作用。javaclass Team implements Cloneable { String name; ArrayList<String> members; // 可变引用类型 @Override public Object clone() { try { Team cloned = (Team) super.clone(); // 必须手动对可变引用字段再次 clone // 否则 cloned.members 和 this.members 指向同一个 List cloned.members = (ArrayList<String>) this.members.clone(); return cloned; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } }protected 访问权限的限制:
Object中的clone()是protected的。这意味着你不能直接调用外部对象的clone()方法,除非该对象明确重写并公开了该方法。javaObject obj = new Object(); // obj.clone(); // 编译错误!'clone()' has protected access in 'java.lang.Object'Final 字段的冲突:
如果类中包含
final修饰的可变引用字段,实现深拷贝将非常困难。因为final字段一旦赋值(在super.clone()的浅拷贝阶段已赋值),就无法在clone方法中再次赋值(指向新的深度拷贝对象)。- 解决方案:去掉
final修饰符,或者不使用clone()机制。
- 解决方案:去掉
扩展知识:
最佳实践:拷贝构造器 (Copy Constructor):
鉴于
clone()的诸多设计缺陷(异常检查、类型转换、浅拷贝陷阱、构造器绕过),《Effective Java》建议慎用 clone,推荐使用拷贝构造器或静态工厂方法。java// 推荐方案:清晰、可控 public class User { private String name; private Date birth; // 拷贝构造器 public User(User other) { this.name = other.name; // 手动处理深拷贝逻辑,清晰明了 this.birth = new Date(other.birth.getTime()); } }序列化实现深拷贝:
对于复杂的对象图,手动递归重写
clone非常容易出错。一种“偷懒”但性能较低的深拷贝方式是利用序列化(Serialization)。- 原理:将对象写出到字节流,再从字节流读回来。
- 工具:
Apache Commons Lang的SerializationUtils.clone(obj)或使用 JSON 库(Jackson/Gson)进行转存。
wait()
public final void wait() throws InterruptedException:导致当前线程进入等待状态,直到另一个线程调用此对象的 notify() 或 notifyAll() 方法。
重载版本:
wait(long timeout):等待指定毫秒数。wait(long timeout, int nanos):等待指定毫秒数加纳秒数。
返回:
void。抛出:
InterruptedException- 如果任何线程在当前线程等待之前或期间中断了当前线程。IllegalMonitorStateException- 如果当前线程不是此对象监视器(锁)的所有者。
基本示例:
标准等待/通知模式(Producer-Consumer 简化版):
此示例展示了 wait() 必须配合 synchronized 和 while 循环使用的标准范式。
public class WaitNotifyDemo {
private static final Object lock = new Object();
private static boolean isReady = false;
public static void main(String[] args) {
Thread consumer = new Thread(() -> {
synchronized (lock) {
// 1. 必须在循环中检查条件 (防止虚假唤醒)
while (!isReady) {
try {
System.out.println("Consumer: 条件不满足,开始 wait...");
// 2. 释放锁并挂起
lock.wait();
System.out.println("Consumer: 被唤醒,重新获得锁");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Consumer: 执行业务逻辑");
}
});
Thread producer = new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
synchronized (lock) {
System.out.println("Producer: 准备数据...");
isReady = true;
// 3. 唤醒等待线程(注意:此时锁并未立即释放,需执行完同步块)
lock.notify();
System.out.println("Producer: 已发出通知");
}
});
consumer.start();
producer.start();
}
}核心特性:
Monitor 机制与 Wait Set:
Java 对象头(Object Header)关联了一个监视器(Monitor)。Monitor 内部维护了两个重要的队列:
- Entry Set(锁池):等待获取锁的线程队列。
- Wait Set(等待池):调用了
wait()的线程队列。
当线程调用
wait()时,它会:- 释放当前持有的对象锁(这是与
Thread.sleep的核心区别)。 - 进入该对象的 Wait Set。
- 线程状态变为
WAITING或TIMED_WAITING,并不再参与 CPU 调度。
只有当其他线程调用
notify()(随机唤醒一个)或notifyAll()(唤醒所有)时,线程才会从 Wait Set 移动到 Entry Set,重新竞争锁。虚假唤醒(Spurious Wakeup):
操作系统层面的条件变量(Condition Variable)实现可能会在没有收到
notify信号的情况下莫名其妙地唤醒线程。这是底层 OS 的一种允许行为(通常为了性能或处理信号)。因此,Java 规范强制要求:
wait()应该总是在循环中调用。java// 正确写法:循环检查 synchronized (obj) { while (<condition does not hold>) obj.wait(); // Perform action appropriate to condition } // 错误写法:if 检查 synchronized (obj) { if (<condition does not hold>) obj.wait(); // 如果发生虚假唤醒,或者被错误唤醒,这里将直接执行,导致逻辑错误 // ... }原子性释放:
wait()方法不仅挂起线程,还是一个原子操作(Atomic Operation),它保证了“释放锁”和“进入等待状态”之间不会插入其他指令。这避免了“丢失信号”的问题(即在判断条件和挂起之间,另一个线程刚好发出了 notify)。
注意事项:
IllegalMonitorStateException:
wait()、notify()、notifyAll()必须在同步代码块(synchronized)内部调用,且调用对象必须是锁对象本身。如果不持有该对象的锁直接调用
wait(),JVM 会抛出IllegalMonitorStateException。这是为了确保线程安全地修改共享的条件变量。wait() vs Thread.sleep():
这是最高频的面试题,本质区别在于锁的处理:
特性 Object.wait() Thread.sleep() 所属类 Object(实例方法)Thread(静态方法)锁行为 释放当前持有的锁 不释放任何锁 (抱着锁睡觉) 使用场景 线程间通信/协作 暂停执行/时间控制 唤醒方式 notify(),notifyAll(), 中断时间到, 中断 前置条件 必须持有 Monitor 锁 无需持有锁 java// 错误示范:在 synchronized 中 sleep 导致死锁风险 synchronized(lock) { // 其他线程无法获得 lock,整个系统卡死 Thread.sleep(10000); }
扩展知识:
JUC Condition (现代替代方案):
在 JDK 5 引入
java.util.concurrent包后,推荐使用Lock和Condition接口来替代原始的wait/notify。Condition提供了更灵活的等待队列:多路通知:一个 Lock 可以创建多个 Condition(例如
notFull和notEmpty),从而实现精准唤醒(例如生产者只唤醒消费者),而Object.notify是随机唤醒,效率较低。await() / signal():对应
wait()/notify()。javaLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); lock.lock(); try { while (!ready) { condition.await(); // 释放 lock } } finally { lock.unlock(); }
notify()
public final void notify():唤醒在此对象监视器(Monitor)上等待的单个线程。
- 返回:
void。 - 抛出:
IllegalMonitorStateException- 如果当前线程不是此对象监视器的所有者(即没有在synchronized块中调用)。
基本示例:
精准唤醒(配合 wait 使用):
此示例展示了 notify() 如何将挂起的线程“复活”。注意观察输出顺序,证明 notify 后并不会立即释放锁。
public class NotifyDemo {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("T1: 获得锁,准备 wait");
try {
lock.wait(); // 释放锁,进入 Wait Set
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T1: 被唤醒,重新获得锁,继续执行");
}
});
Thread t2 = new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
synchronized (lock) {
System.out.println("T2: 获得锁,准备 notify");
lock.notify(); // 唤醒 T1,但 T2 此时仍持有锁
System.out.println("T2: 已发出通知,但还要睡 2秒...");
try { Thread.sleep(2000); } catch (InterruptedException e) {}
System.out.println("T2: 释放锁");
} // T2 退出同步块后,T1 才有机会竞争锁
});
t1.start();
t2.start();
}
}核心特性:
监视器状态迁移(Wait Set -> Entry Set):
notify()的本质操作并非立即运行线程,而是改变线程的存储位置。- Wait Set(等待池):调用
wait()的线程在此休眠。 - Entry Set(锁池):等待获取锁的线程在此排队。
当调用
obj.notify()时,JVM 会随机(取决于具体实现,通常不可预测)从obj的 Wait Set 中选择一个线程,将其移动到 Entry Set。关键点:被唤醒的线程不会立即执行,它必须等待当前持有锁的线程(即调用
notify的线程)释放锁,然后在 Entry Set 中与其他线程再次竞争锁的所有权。- Wait Set(等待池):调用
选择策略的任意性:
Java 规范并未规定
notify()必须唤醒哪一个等待线程(例如,不保证是等待时间最长的那个)。这意味着在特定的 JVM 实现中,这种选择可能是随机的。结论:如果你依赖特定的唤醒顺序,绝对不要使用
notify(),而应使用Lock+Condition或其他并发工具。非即时释放锁:
这是一个极其常见的误区。调用
notify()是一条极其轻量的指令,它不会导致当前线程释放锁。当前线程必须执行完synchronized代码块的所有剩余语句,或者显式调用wait(),锁才会被释放,被唤醒的线程才真正有机会运行。javasynchronized(lock) { lock.notify(); // 此时被唤醒的线程依然被阻塞在 Entry Set 中 // 因为当前线程还没有走出 synchronized 块 heavyCalculation(); } // 代码块结束,锁释放,被唤醒线程开始竞争
注意事项:
信号丢失(Missed Signal)风险:
如果
notify()在wait()之前执行,该信号将被直接丢弃。因为notify针对的是当前的 Wait Set,如果 Wait Set 为空,这个通知就“虽然发出了但无人接收”。这会导致后来的wait()线程永远等待下去。解决方案:始终使用变量标识状态(如
boolean isReady),并在wait前检查该状态(即标准的while循环模式)。Notify vs NotifyAll(死锁风险):
这是
notify()最危险的坑。notify()每次只唤醒一个线程。在“多生产者-多消费者”场景下,如果所有线程都在同一个锁上等待,极易发生信号劫持。场景:
- 消费者 A 被唤醒,消费了一个元素,释放锁并发出
notify()。 - 本意是唤醒生产者 B 生产数据。
- 结果运气不好,唤醒了另一个消费者 C。
- 消费者 C 发现队列为空,于是
wait()。 - 此时,所有线程(包括生产者)都在
wait,系统陷入死锁。
黄金法则:除非你非常确定只唤醒同一类线程(且每次唤醒一个就能满足逻辑),否则永远优先使用
notifyAll()。- 消费者 A 被唤醒,消费了一个元素,释放锁并发出
扩展知识:
Condition.signal() 的优势:
为了解决
Object.notify()无法区分唤醒对象的问题,JDK 5 引入了Condition。javaReentrantLock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); // 专门存放生产者的队列 Condition notEmpty = lock.newCondition(); // 专门存放消费者的队列 // 生产者只唤醒消费者,避免了唤醒同类的无用功 notEmpty.signal();
notifyAll()
public final void notifyAll():唤醒在此对象监视器(Monitor)上等待的所有线程。
- 返回:
void。 - 抛出:
IllegalMonitorStateException- 如果当前线程不是此对象监视器的所有者(即没有在synchronized块中调用)。
基本示例:
“发令枪”模式(One-Shot Latch):
此示例演示了 notifyAll() 如何一次性激活所有挂起的线程。与之相比,notify() 只会唤醒其中一个,导致其他线程“永久沉睡”。
public class NotifyAllDemo {
private static final Object lock = new Object();
private static boolean startSignal = false;
public static void main(String[] args) throws InterruptedException {
// 启动 5 个运动员线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + " 准备就绪,等待发令...");
while (!startSignal) {
lock.wait(); // 所有线程在此挂起
}
System.out.println(Thread.currentThread().getName() + " 起跑!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Athlete-" + i).start();
}
Thread.sleep(1000); // 确保所有运动员都已进入 wait 状态
synchronized (lock) {
System.out.println("裁判:各就各位... 预备... 跑!(调用 notifyAll)");
startSignal = true;
lock.notifyAll(); // 唤醒所有等待在 lock 上的 5 个线程
}
}
}核心特性:
全量迁移(Wait Set -> Entry Set):
调用
notifyAll()时,JVM 会将该对象 Wait Set(等待池) 中的所有线程全部移入 Entry Set(锁池)。- 状态变更:线程状态从
WAITING变为BLOCKED。 - 竞争机制:这些被唤醒的线程不会同时运行,而是必须在当前线程释放锁之后,在 Entry Set 中与其他线程(以及可能新来的线程)共同竞争这把锁。
- 执行顺序:最终只有一个线程能抢到锁并执行
wait()之后的代码,其他失败者继续在 Entry Set 中阻塞等待下一次锁释放。
- 状态变更:线程状态从
解决“信号劫持”与死锁:
这是
notifyAll()存在的最大意义。在多生产者-多消费者场景中,如果使用notify(),可能会出现“消费者唤醒了另一个消费者”的情况(同类唤醒),导致生产者一直处于等待状态,最终所有线程都在等待,形成死锁(Deadlock)。使用
notifyAll()的逻辑:- 虽然唤醒了所有消费者和生产者,效率看似低了。
- 但是,它保证了至少有一个正确的线程(比如生产者)被唤醒并能够推进状态(生产数据),从而打破僵局。
- 结论:除了极简单的 1 对 1 通信,默认应使用
notifyAll()以确保程序的活跃性(Liveness)。
注意事项:
惊群效应(Thundering Herd Problem):
这是
notifyAll()的主要性能弊端。- 现象:你唤醒了 100 个线程,但只有一个能抢到锁并处理任务(例如队列里只来了一个数据)。
- 代价:剩下的 99 个线程虽然被唤醒了,但抢锁失败后又得重新阻塞,或者抢到锁后发现条件不满足(
while循环检查)又调用wait()挂起。 - 结果:导致大量的 CPU 上下文切换(Context Switch)和无效的锁竞争,造成系统负载瞬间飙升。
必须配合 while 循环:
即使使用了
notifyAll(),所有被唤醒的线程也会依次获取锁。- 线程 A 抢到锁,消费了数据,将条件置为 false。
- 线程 A 释放锁。
- 线程 B(也被
notifyAll唤醒了)抢到锁。 - 如果线程 B 使用
if判断,它会直接往下执行(即使数据已经被 A 消费完了),导致逻辑错误。 - 因此,必须使用
while循环,让 B 醒来后再次检查条件,发现没数据了,乖乖回去wait()。
扩展知识:
JUC Condition 的精准通知:
为了解决
notifyAll()的“惊群效应”,java.util.concurrent.locks.Condition提供了分组唤醒机制。lock.newCondition()可以创建多个等待队列。优化方案:将生产者放在
notFull队列,消费者放在notEmpty队列。精准打击:生产者生产完数据,只调用
notEmpty.signalAll()(唤醒所有消费者),而不打扰其他生产者。java// 优化的架构 private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); // ... 生产逻辑 notEmpty.signal(); // 只通知消费者 } finally { lock.unlock(); } }
finalize()@Deprecated
protected void finalize() throws Throwable:[已废弃] 当垃圾回收器(GC)确定不存在对该对象的更多引用时,由 GC 在对象上调用此方法。子类可以重写finalize方法来处理系统资源的释放或执行其他清理操作。
- 返回:
void。 - 抛出:
Throwable— 此方法抛出的任何异常都会被 GC 线程捕获并忽略,不会导致程序终止。 - 状态:Deprecated (Since Java 9, Marked for Removal)。在 JEP 421 中被标记为“终结过时”,未来版本将被彻底删除。
基本示例:
模拟资源回收(仅作原理演示,严禁生产使用):
此代码演示了 finalize 的触发机制及其执行的不确定性。
public class FinalizeDemo {
static class HeavyResource {
private String name;
public HeavyResource(String name) {
this.name = name;
}
// 子类可以重写`finalize`方法来处理系统资源的释放或执行其他清理操作。
@Override
protected void finalize() throws Throwable {
// 务必调用 super.finalize(),虽然 Object 中是空的,但这是好习惯
super.finalize();
System.out.println("Resource [" + name + "] is being finalized by thread: "
+ Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
HeavyResource resource = new HeavyResource("DB-Connection");
// 1. 断开引用,使其变为不可达(Unreachable)
resource = null;
// 2. 建议 JVM 进行垃圾回收(仅是建议,不保证立即执行)
System.gc(); // 非阻塞的异步方法
// 3. 必须等待,因为 Finalizer 线程优先级极低
System.out.println("Main thread waiting...");
Thread.sleep(1000);
}
}核心特性:
F-Queue 与 Finalizer 线程机制(GC 回收过程):
当一个重写了
finalize()方法的对象被创建时,JVM 会创建一个额外的java.lang.ref.Finalizer对象指向它。- 第一阶段(标记):GC 发现对象不可达,且该对象重写了
finalize方法,将其放入 F-Queue(Finalization Queue)。 - 第二阶段(执行):一个低优先级的守护线程(
Finalizer线程)会不断从 F-Queue 中取出对象,并执行其finalize()方法。 - 第三阶段(回收):只有在
finalize()执行完毕后,GC 再次扫描发现该对象依然不可达,才会真正回收其内存。
这意味着:Finalize 对象至少需要两次 GC 周期才能被回收,这严重拖慢了回收效率。
- 第一阶段(标记):GC 发现对象不可达,且该对象重写了
对象复活(Object Resurrection):
这是
finalize最诡异的特性。在finalize()方法中,你可以把this赋值给某个全局静态变量,从而让对象“死而复生”。单次执行保证:JVM 保证每个对象的
finalize()只会被调用一次。如果对象复活后再次变成不可达,GC 将直接回收它,不再调用finalize()。javapublic class Zombie { public static Zombie INSTANCE; @Override protected void finalize() { System.out.println("Resurrecting..."); INSTANCE = this; // 复活!重新建立强引用 } }
注意事项:
不确定性与性能灾难:
- 执行时机不确定:你无法保证
finalize何时运行,甚至无法保证它一定会运行(例如 JVM 直接退出时)。因此,绝不能用它来释放关键资源(如数据库连接、文件句柄),否则极易导致资源耗尽(Resource Leak)。 - 吞吐量下降:创建带有
finalize的对象比较慢(需要注册),且回收需要至少两个 GC 周期。在大量创建此类对象的场景下,由于Finalizer线程优先级低,处理速度可能赶不上创建速度,导致 F-Queue 堆积,最终引发OutOfMemoryError。
- 执行时机不确定:你无法保证
异常被吞没:
如果
finalize()方法中抛出未捕获的异常,该异常会被忽略,且不会打印任何堆栈信息。这使得调试变得极其困难,系统可能处于一种损坏的中间状态而无人知晓。安全漏洞(Finalizer Attack):
如果一个类的构造函数抛出异常(例如权限检查失败),对象本不应被创建。但如果有恶意子类覆盖了
finalize,该方法仍会被执行,攻击者可以在其中获取部分初始化的对象实例,绕过安全检查。- 防御:将类声明为
final,或在finalize中抛出异常(虽然没用),最佳方案是使用final的finalize方法阻止重写:protected final void finalize() {}。
- 防御:将类声明为
扩展知识:
- 替代方案(Best Practices):
AutoCloseable + try-with-resources(首选):
显式管理资源生命周期,确定性强,代码清晰。
javatry (FileInputStream fis = new FileInputStream("file.txt")) { // 使用资源 } // 自动调用 close()java.lang.ref.Cleaner (Java 9+):
finalize的官方替代品。它利用虚引用(PhantomReference)机制,比finalize更轻量、更安全(无法复活对象),但依然不保证立即执行。通常用于兜底(Safety Net)。
补充
IDEA 中查看 JDK 源码
在 IntelliJ IDEA 中查看 JDK 源码(例如 String, ArrayList, Object 的底层实现)是 Java 进阶必经之路。IDEA 对此支持得非常完美。
以下是三种最常用的方法,以及如何解决“只能看反编译代码(.class)看不到源码(.java)”的问题。
方法1:快捷键/鼠标跳转
方法一:快捷键/鼠标跳转(最常用):
这是日常开发中最快的方式。当你代码中写到了某个类(比如 String)或者某个方法(比如 System.out.println)时:
鼠标操作:
Ctrl + 左击按住
Ctrl(Mac 是Command) 键。鼠标移动到代码上的类名或方法名上(文字会变成超链接样式)。
点击左键,直接跳转。
键盘操作:
Ctrl + B光标停留在类名或方法名上。
按下
Ctrl + B(Windows/Linux) 或Command + B(Mac)。
方法2:全局搜索
方法二:全局搜索(想直接看某个类):
如果你当前代码里没有用到 HashMap,但你想专门去看看它的源码:
按下
Ctrl + N(Windows/Linux) 或Command + O(Mac)。这是 "Go to Class"(查找类)功能。- 或者双击
Shift打开 "Search Everywhere"。
- 或者双击
输入类名,例如
HashMap。关键点: 确保勾选右上角的 "Include non-project items" (包含非项目文件) 或者再次按下快捷键,这样才能搜到 JDK 里的类。
回车进入。
方法3:从项目结构树查看
方法三:从项目结构树查看(浏览全貌):
如果你想浏览 JDK 到底包含了哪些包(Package):
- 打开左侧的 Project 面板。
- 拉到最下方,找到 External Libraries (外部库)。
- 展开
<1.8>或<11>(取决于你的 JDK 版本)。 - 展开
rt.jar(JDK 8) 或java.base(JDK 9+)。 - 在这里你可以像翻文件夹一样浏览
java.lang,java.util等包下的所有源码。
问题:为什么看的是 .class 文件
问题:为什么我看到的是 ".class" 文件:
如果你点进去后,看到 IDEA 顶部提示 "Decompiled .class file, bytecode version...",并且代码里没有注释,变量名可能是 var1, var2 这种奇怪的名字。
原因: IDEA 只找到了编译后的字节码,没有关联到 JDK 的源码压缩包 (src.zip)。
解决方法:
方案 A:点击提示栏的 "Download Sources":
IDEA 通常会在顶部弹出一个黄色条,点击 "Download Sources" 或 "Choose Sources...",它会自动尝试下载。
方案 B:手动关联(一劳永逸):
如果自动下载失败,说明你需要手动指定一下本地的源码包(通常安装 JDK 时都自带了)。
- 打开 File -> Project Structure (快捷键
Ctrl+Alt+Shift+S/Cmd+;)。 - 选择左侧的 SDKs (或 Platform Settings -> SDKs)。
- 选中你当前使用的 JDK 版本。
- 点击右侧的 Sourcepath 标签页。
- 点击
+号,找到你 JDK 的安装目录。 - 选择目录下的
src.zip文件(如果是 Linux 可能是lib/src.zip)。 - 点击 OK 应用。

神器技巧
Pro Tips:看源码的神器技巧:
查看大纲 (Structure): JDK 源码通常很长(
String有 3000 多行),直接看很累。- 按下
Alt + 7(Windows) /Command + 7(Mac) 打开 Structure 面板。 - 这里列出了所有的方法和属性,可以直接点击跳转。
- 按下
查看类图 (Diagram): 想搞清继承关系?
- 在类名上右键 -> Diagrams -> Show Diagram。
- 它会画出这个类的父类、接口实现关系图。
看具体的实现类 (Implementation): 如果你点击
List接口的方法,它只会带你到接口定义。你想看ArrayList是怎么实现的?- 使用
Ctrl + Alt + B(Windows) /Command + Option + B(Mac)。 - 选择具体的实现类(如
ArrayList)。
- 使用
您现在可以试着去 IDEA 里按住 Ctrl 点一下 String 类,看看它的 equals 方法是怎么写的(你会发现之前学的 instanceof 和数组比较都在里面)!